package com.iocup.keybastion.authentication.impl;


import com.iocup.keybastion.authentication.AccessToken;
import com.iocup.keybastion.authentication.TokenBuilder;
import com.iocup.keybastion.authentication.TokenService;
import com.iocup.keybastion.common.AuthConstant;
import com.iocup.keybastion.configuration.TokenProperties;
import com.iocup.keybastion.context.SecurityContextHolder;
import com.iocup.keybastion.authentication.RefreshToken;

import com.iocup.keybastion.core.lock.LocalLock;
import com.iocup.keybastion.core.lock.LockType;
import com.iocup.keybastion.core.profile.UserProfile;
import com.iocup.keybastion.core.session.Session;
import com.iocup.keybastion.core.store.CacheStore;
import com.iocup.keybastion.core.lock.Lock;
import com.iocup.keybastion.core.session.DefaultSessionIdGenerator;
import com.iocup.keybastion.core.session.SessionIdGenerator;
import com.iocup.keybastion.exception.AuthenticationException;

import com.pine.sunflower.core.validate.Validator;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.List;

/**
 * 默认的token服务类
 *
 * @author xyjxust
 * @create 2022/3/4 15:28
 **/
public class DefaultTokenService implements TokenService {

    /**
     * token构建器
     */
    private TokenBuilder tokenBuilder;
    /**
     * 缓存存储类
     */
    private CacheStore cacheStore;
    /**
     * sessionId生成器
     */
    private SessionIdGenerator sessionIdGenerator;
    /**
     * token的相关配置
     */
    private TokenProperties tokenProperties;
    /**
     * 锁，主要用于
     */
    private Lock lock;

    private static final String TOKEN_SESSION_KEY = AuthConstant.CACHE_NAMESPACE + "TOKEN:SESSION:";

    private static final String SESSION_ID_KEY = AuthConstant.CACHE_NAMESPACE + "SESSION:ID:SESSION:";

    private static final String REFRESH_SESSION_KEY = AuthConstant.CACHE_NAMESPACE + "REFRESH:SESSION:";

    private static final String LOGIN_NAME_SESSION_KEY = AuthConstant.CACHE_NAMESPACE + "LOGIN:NAME:SESSION:";

    private static final String TICKET_TOKEN_KEY = AuthConstant.CACHE_NAMESPACE + "TICKET:TOKEN:";


    public DefaultTokenService(Lock lock, TokenProperties tokenProperties, TokenBuilder tokenBuilder, SessionIdGenerator sessionIdGenerator, CacheStore cacheStore) {
        Validator.build().validate(tokenBuilder == null, "tokenBuilder不能为空")
                .validate(cacheStore == null, "tokenStore不能为空")
                .validate(tokenProperties == null, "tokenProperties不能为空")
                .throwIfFail();
        this.lock = lock;
        //设置过来的为空就默认使用本地锁
        if (lock == null) {
            this.lock = new LocalLock();
        }
        this.tokenProperties = tokenProperties;
        this.tokenBuilder = tokenBuilder;
        this.cacheStore = cacheStore;
        this.sessionIdGenerator = sessionIdGenerator;
        if (sessionIdGenerator == null) {
            this.sessionIdGenerator = new DefaultSessionIdGenerator();
        }
    }


    private AccessToken createToken(UserProfile userProfile, RefreshToken refreshToken) {
        return this.tokenBuilder.build(userProfile, refreshToken);
    }

    @Override
    public Session createAndSave(UserProfile userProfile) {
        AccessToken accessToken = this.tokenBuilder.build(userProfile);
        Session session = Session.build().id(createSessionId(userProfile.getLoginName(), userProfile.getDevice())).userProfile(userProfile).token(accessToken);
        this.save(session);
        return session;
    }

    @Override
    public Session createAndSaveByRefresh(String refreshTokenValue) {
        Session session = getByRefreshToken(refreshTokenValue);
        if (session == null) {
            throw new AuthenticationException("refresh_token错误或者刷新token已过期");
        }
        //通过token查询session信息
        Session tokenSession = getByToken(session.getToken().getAccessToken());
        //如果为空就从小创建token信息
        if (tokenSession == null) {
            //先把刷新token获取出来
            RefreshToken refreshToken = session.getToken().getRToken();
            AccessToken token = session.getToken();
            //刷新token不继续使用现有的过期token或者token为空
            if (token == null || !this.tokenProperties.isRefreshUseOldToken()) {
                token = createToken(session.getUserProfile(), refreshToken);
            } else {
                token = this.tokenBuilder.build(token, session.getUserProfile());
            }
            session = Session.build().id(session.getId()).token(token).userProfile(session.getUserProfile());
            this.save(session);
            return session;
        }
        return session;
    }


    @Override
    public void save(Session session) {
        this.lock.tryLock(LockType.SESSION_ID, session.getId(), s -> {
            //如果同一账号不允许一起登录
            if (!this.tokenProperties.isConcurrent()) {
                //获取该登录人的旧session
                Session oldSession = getBySessionId(session.getId());
                //存在就删除旧session
                if (oldSession != null) {
                    removeByToken(oldSession.getToken().getAccessToken());
                }
                //重新保存新的session
                doSave(session);
                return;
            }
            //如果同一账号允许一起登录，并且允许每次登录新建一个token
            if (!this.tokenProperties.isShare()) {
                doSave(session);
                return;
            }
            //如果同一账号允许一起登录，并且不允许每次登录新建一个token，那么先把旧session拿出来
            Session oldSession = getBySessionId(session.getId());
            if (oldSession != null) {
                //不为空时使用旧token
                session.setToken(oldSession.getToken());
            }
            doSave(session);

        });
    }


    private void doSave(Session session) {

        //获取token的过期时间
        long expireTime = Long.valueOf((session.getToken().getExpireTime().getTime()
                - System.currentTimeMillis()) / 1000L).intValue();
        //存储token-->session
        this.cacheStore.setValue(buildTokenSessionKey(session.getToken().getAccessToken()),
                session, expireTime);
        //存储id-->session
        this.cacheStore.setValue(buildSessionIdKey(session.getId()), session, expireTime);
        //存储loginName+id-->id
        this.cacheStore.setValue(buildLoginNameSessionKey(session.getUserProfile().getLoginName(), session.getId()), session.getId(), expireTime);
        //存储刷新token
        if (StringUtils.isNotBlank(session.getToken().getRefreshToken())) {
            expireTime = Long.valueOf((session.getToken().getRToken().getExpiration().getTime()
                    - System.currentTimeMillis()) / 1000L).intValue();
            this.cacheStore.setValue(buildRefreshTokenSessionKey(session.getToken().getRefreshToken()), session, expireTime);
        }

    }

    @Override
    public void removeByToken(String token) {
        removeBySession(this.cacheStore.getValue(buildTokenSessionKey(token)));
    }


    public void removeBySession(Session session) {
        if (session != null) {
            this.lock.tryLock(LockType.SESSION_ID, session.getId(), s -> {
                //删除token绑定的session
                this.cacheStore.removeValue(buildTokenSessionKey(session.getToken().getAccessToken()));
                //删除sessionId绑定的session
                this.cacheStore.removeValue(buildSessionIdKey(session.getId()));
                //删除登录名绑定的sessionId
                this.cacheStore.removeValue(buildLoginNameSessionKey(session.getUserProfile().getLoginName(), session.getId()));
                //如果刷新token存在，那么删除刷新token绑定的token
                if (session.getToken().getRToken() != null) {
                    this.cacheStore.removeValue(buildRefreshTokenSessionKey(session.getToken().getRefreshToken()));
                }
            });
        }
    }

    @Override
    public void removeByLoginName(String loginName) {
        this.lock.tryLock(LockType.LOGIN_NAME, loginName, s -> {
            List<String> ids = this.cacheStore.queryPrefixKey(buildLoginNameSessionPreKey(loginName), -1, 0);
            if (CollectionUtils.isNotEmpty(ids)) {
                for (String id : ids) {
                    removeBySession(this.cacheStore.getValue(buildSessionIdKey(this.cacheStore.getValue(id))));
                }
            }
        });
    }

    @Override
    public Session getByToken(String token) {
        return this.cacheStore.getValue(buildTokenSessionKey(token));
    }

    @Override
    public Session getBySessionId(String sessionId) {
        return this.cacheStore.getValue(buildSessionIdKey(sessionId));
    }

    @Override
    public Session getByRefreshToken(String refreshToken) {
        return this.cacheStore.getValue(buildRefreshTokenSessionKey(refreshToken));
    }

    @Override
    public String extractKey(String loginName, String device, String... otherKeys) {
        return createSessionId(loginName, device, otherKeys);
    }

    @Override
    public String extractKey(String loginName, String device) {
        return createSessionId(loginName, device);
    }

    private String createSessionId(String loginName, String device) {
        //如果同一账号不允许一起登录并且是非共享同一个token
        if (this.tokenProperties.isConcurrent() && !this.tokenProperties.isShare()) {
            //获取ip和浏览器标识生成不同的sessionId
            return createSessionId(loginName, device, SecurityContextHolder.getContext().getWebContextHolder().getRemoteAddress(),
                    SecurityContextHolder.getContext().getWebContextHolder().getRequestHeader("User-Agent").orElse("").toLowerCase());
        }
        return sessionIdGenerator.extractKey(loginName, device);
    }

    private String createSessionId(String loginName, String device, String... otherKeys) {
        return sessionIdGenerator.extractKey(loginName, device, otherKeys);
    }


    private String buildTokenSessionKey(String token) {
        return String.format("%s%s", TOKEN_SESSION_KEY, token);
    }

    private String buildSessionIdKey(String id) {
        return String.format("%s%s", SESSION_ID_KEY, id);
    }

    private String buildRefreshTokenSessionKey(String token) {
        return String.format("%s%s", REFRESH_SESSION_KEY, token);
    }

    private String buildLoginNameSessionKey(String loginName, String id) {
        return String.format("%s%s:%s", LOGIN_NAME_SESSION_KEY, loginName, id);
    }

    private String buildLoginNameSessionPreKey(String loginName) {
        return String.format("%s%s:", LOGIN_NAME_SESSION_KEY, loginName);
    }

    private String buildTicketTokenKey(String ticket) {
        return String.format("%s%s:", TICKET_TOKEN_KEY, ticket);
    }
}
